//go:build !integration
// +build !integration

package completion

import (
	"bytes"
	"compress/gzip"
	"context"
	"errors"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/Jguer/yay/v12/pkg/db"
	"github.com/Jguer/yay/v12/pkg/db/mock"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const samplePackageResp = `
# AUR package list, generated on Fri, 24 Jul 2020 22:05:22 GMT
cytadela
bitefusion
globs-svn
ri-li
globs-benchmarks-svn
dunelegacy
lumina
eternallands-sound
`

const expectPackageCompletion = `cytadela	AUR
bitefusion	AUR
globs-svn	AUR
ri-li	AUR
globs-benchmarks-svn	AUR
dunelegacy	AUR
lumina	AUR
eternallands-sound	AUR
`

type mockDoer struct {
	t                *testing.T
	returnBody       []byte
	returnStatusCode int
	returnErr        error
	wantURL          string
}

func (m *mockDoer) Get(url string) (*http.Response, error) {
	assert.Equal(m.t, m.wantURL, url)
	return &http.Response{
		StatusCode: m.returnStatusCode,
		Body:       io.NopCloser(bytes.NewReader(m.returnBody)),
	}, m.returnErr
}

func gzipString(s string) []byte {
	var buf bytes.Buffer
	gz := gzip.NewWriter(&buf)
	gz.Write([]byte(s))
	gz.Close()
	return buf.Bytes()
}

func Test_createAURList(t *testing.T) {
	t.Parallel()
	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 200,
		returnBody:       []byte(samplePackageResp),
		returnErr:        nil,
	}
	out := &bytes.Buffer{}
	err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
	assert.NoError(t, err)
	gotOut := out.String()
	assert.Equal(t, expectPackageCompletion, gotOut)
}

func Test_createAURListGzip(t *testing.T) {
	t.Parallel()
	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 200,
		returnBody:       gzipString(samplePackageResp),
		returnErr:        nil,
	}
	out := &bytes.Buffer{}
	err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
	assert.NoError(t, err)
	gotOut := out.String()
	assert.Equal(t, expectPackageCompletion, gotOut)
}

func Test_createAURListHTTPError(t *testing.T) {
	t.Parallel()
	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 200,
		returnBody:       []byte(samplePackageResp),
		returnErr:        errors.New("Not available"),
	}

	out := &bytes.Buffer{}
	err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
	assert.EqualError(t, err, "Not available")
}

func Test_createAURListStatusError(t *testing.T) {
	t.Parallel()
	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 503,
		returnBody:       []byte(samplePackageResp),
		returnErr:        nil,
	}

	out := &bytes.Buffer{}
	err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
	assert.EqualError(t, err, "invalid status code: 503")
}

func TestNeedsUpdate(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		setupFile      func(t *testing.T, path string)
		interval       int
		force          bool
		expectedResult bool
	}{
		{
			name:           "force returns true",
			setupFile:      nil,
			interval:       7,
			force:          true,
			expectedResult: true,
		},
		{
			name:           "file does not exist returns true",
			setupFile:      nil,
			interval:       7,
			force:          false,
			expectedResult: true,
		},
		{
			name: "fresh file returns false",
			setupFile: func(t *testing.T, path string) {
				t.Helper()
				err := os.WriteFile(path, []byte("test"), 0o600)
				require.NoError(t, err)
			},
			interval:       7,
			force:          false,
			expectedResult: false,
		},
		{
			name: "interval -1 never updates",
			setupFile: func(t *testing.T, path string) {
				t.Helper()
				err := os.WriteFile(path, []byte("test"), 0o600)
				require.NoError(t, err)
				// Set file time to 30 days ago
				oldTime := time.Now().Add(-30 * 24 * time.Hour)
				err = os.Chtimes(path, oldTime, oldTime)
				require.NoError(t, err)
			},
			interval:       -1,
			force:          false,
			expectedResult: false,
		},
		{
			name: "old file returns true",
			setupFile: func(t *testing.T, path string) {
				t.Helper()
				err := os.WriteFile(path, []byte("test"), 0o600)
				require.NoError(t, err)
				// Set file time to 10 days ago
				oldTime := time.Now().Add(-10 * 24 * time.Hour)
				err = os.Chtimes(path, oldTime, oldTime)
				require.NoError(t, err)
			},
			interval:       7,
			force:          false,
			expectedResult: true,
		},
		{
			name: "file within interval returns false",
			setupFile: func(t *testing.T, path string) {
				t.Helper()
				err := os.WriteFile(path, []byte("test"), 0o600)
				require.NoError(t, err)
				// Set file time to 3 days ago
				oldTime := time.Now().Add(-3 * 24 * time.Hour)
				err = os.Chtimes(path, oldTime, oldTime)
				require.NoError(t, err)
			},
			interval:       7,
			force:          false,
			expectedResult: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			tmpDir := t.TempDir()
			completionPath := filepath.Join(tmpDir, "completion")

			if tt.setupFile != nil {
				tt.setupFile(t, completionPath)
			}

			result := NeedsUpdate(completionPath, tt.interval, tt.force)
			assert.Equal(t, tt.expectedResult, result)
		})
	}
}

// mockPkgSynchronizer implements PkgSynchronizer for testing.
type mockPkgSynchronizer struct {
	packages []db.IPackage
}

func (m *mockPkgSynchronizer) SyncPackages(...string) []db.IPackage {
	return m.packages
}

func Test_createRepoList(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		packages       []db.IPackage
		expectedOutput string
		expectedError  error
	}{
		{
			name:           "empty package list",
			packages:       []db.IPackage{},
			expectedOutput: "",
			expectedError:  nil,
		},
		{
			name: "single package",
			packages: []db.IPackage{
				&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
			},
			expectedOutput: "vim\textra\n",
			expectedError:  nil,
		},
		{
			name: "multiple packages",
			packages: []db.IPackage{
				&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
				&mock.Package{PName: "git", PDB: mock.NewDB("extra")},
				&mock.Package{PName: "linux", PDB: mock.NewDB("core")},
			},
			expectedOutput: "vim\textra\ngit\textra\nlinux\tcore\n",
			expectedError:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			dbExecutor := &mockPkgSynchronizer{packages: tt.packages}
			out := &bytes.Buffer{}

			err := createRepoList(dbExecutor, out)

			if tt.expectedError != nil {
				assert.EqualError(t, err, tt.expectedError.Error())
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.expectedOutput, out.String())
		})
	}
}

// errorWriter is a writer that always returns an error.
type errorWriter struct{}

func (e *errorWriter) Write(p []byte) (n int, err error) {
	return 0, errors.New("write error")
}

func Test_createRepoListWriteError(t *testing.T) {
	t.Parallel()

	dbExecutor := &mockPkgSynchronizer{
		packages: []db.IPackage{
			&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
		},
	}

	err := createRepoList(dbExecutor, &errorWriter{})
	assert.EqualError(t, err, "write error")
}

func TestUpdateCache(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		doer           *mockDoer
		packages       []db.IPackage
		expectedOutput string
		expectError    bool
	}{
		{
			name: "successful update",
			doer: &mockDoer{
				returnStatusCode: 200,
				returnBody:       []byte("# Comment\npkg1\npkg2\n"),
				returnErr:        nil,
			},
			packages: []db.IPackage{
				&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
			},
			expectedOutput: "pkg1\tAUR\npkg2\tAUR\nvim\textra\n",
			expectError:    false,
		},
		{
			name: "AUR fetch error removes file",
			doer: &mockDoer{
				returnStatusCode: 500,
				returnBody:       []byte{},
				returnErr:        nil,
			},
			packages:    []db.IPackage{},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			tmpDir := t.TempDir()
			completionPath := filepath.Join(tmpDir, "subdir", "completion")
			tt.doer.t = t
			tt.doer.wantURL = "https://aur.archlinux.org/packages.gz"

			dbExecutor := &mockPkgSynchronizer{packages: tt.packages}

			err := UpdateCache(context.Background(), tt.doer, dbExecutor, "https://aur.archlinux.org", completionPath, nil)

			if tt.expectError {
				assert.Error(t, err)
				// File should be removed on error
				_, statErr := os.Stat(completionPath)
				assert.True(t, os.IsNotExist(statErr))
			} else {
				require.NoError(t, err)
				content, readErr := os.ReadFile(completionPath)
				require.NoError(t, readErr)
				assert.Equal(t, tt.expectedOutput, string(content))
			}
		})
	}
}

func TestShow(t *testing.T) {
	// Note: Not running in parallel because we need to capture os.Stdout
	tests := []struct {
		name        string
		setupFile   func(t *testing.T, path string)
		doer        *mockDoer
		packages    []db.IPackage
		interval    int
		force       bool
		expectError bool
	}{
		{
			name: "existing fresh file",
			setupFile: func(t *testing.T, path string) {
				t.Helper()
				err := os.WriteFile(path, []byte("cached\tdata\n"), 0o600)
				require.NoError(t, err)
			},
			doer:        nil, // Should not be called
			packages:    nil,
			interval:    7,
			force:       false,
			expectError: false,
		},
		{
			name:      "file needs update",
			setupFile: nil,
			doer: &mockDoer{
				returnStatusCode: 200,
				returnBody:       []byte("# Comment\naur-pkg\n"),
				returnErr:        nil,
			},
			packages: []db.IPackage{
				&mock.Package{PName: "repo-pkg", PDB: mock.NewDB("core")},
			},
			interval:    7,
			force:       false,
			expectError: false,
		},
		{
			name:      "force update",
			setupFile: nil,
			doer: &mockDoer{
				returnStatusCode: 200,
				returnBody:       []byte("# Comment\nforced-pkg\n"),
				returnErr:        nil,
			},
			packages:    []db.IPackage{},
			interval:    7,
			force:       true,
			expectError: false,
		},
		{
			name:      "update cache error",
			setupFile: nil,
			doer: &mockDoer{
				returnStatusCode: 500,
				returnBody:       []byte{},
				returnErr:        nil,
			},
			packages:    []db.IPackage{},
			interval:    7,
			force:       false,
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Not running in parallel because we capture os.Stdout
			tmpDir := t.TempDir()
			completionPath := filepath.Join(tmpDir, "completion")

			if tt.setupFile != nil {
				tt.setupFile(t, completionPath)
			}

			if tt.doer != nil {
				tt.doer.t = t
				tt.doer.wantURL = "https://aur.archlinux.org/packages.gz"
			}

			dbExecutor := &mockPkgSynchronizer{packages: tt.packages}

			// Capture stdout using a pipe
			oldStdout := os.Stdout
			r, w, pipeErr := os.Pipe()
			require.NoError(t, pipeErr)
			os.Stdout = w

			err := Show(context.Background(), tt.doer, dbExecutor, "https://aur.archlinux.org", completionPath, tt.interval, tt.force, nil)

			// Close writer first, then restore stdout, then read
			w.Close()
			os.Stdout = oldStdout

			var buf bytes.Buffer
			_, copyErr := io.Copy(&buf, r)
			r.Close()

			if tt.expectError {
				assert.Error(t, err)
			} else {
				require.NoError(t, err)
				require.NoError(t, copyErr)
				// Verify file exists and has content
				content, readErr := os.ReadFile(completionPath)
				require.NoError(t, readErr)
				assert.NotEmpty(t, content)
			}
		})
	}
}

func TestShowFileOpenError(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	// Use a path that can't be created (directory as file)
	completionPath := filepath.Join(tmpDir, "completion")

	// Create a directory where we expect a file - this will cause OpenFile to fail
	err := os.MkdirAll(completionPath, 0o755)
	require.NoError(t, err)

	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 200,
		returnBody:       []byte("# Comment\npkg\n"),
		returnErr:        nil,
	}

	dbExecutor := &mockPkgSynchronizer{packages: []db.IPackage{}}

	err = Show(context.Background(), doer, dbExecutor, "https://aur.archlinux.org", completionPath, 7, true, nil)
	assert.Error(t, err)
}

func TestUpdateCacheMkdirError(t *testing.T) {
	t.Parallel()

	// Create a file where we expect a directory - this will cause MkdirAll to fail
	tmpDir := t.TempDir()
	blockingFile := filepath.Join(tmpDir, "blocking")
	err := os.WriteFile(blockingFile, []byte("block"), 0o600)
	require.NoError(t, err)

	completionPath := filepath.Join(blockingFile, "subdir", "completion")

	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 200,
		returnBody:       []byte("# Comment\npkg\n"),
		returnErr:        nil,
	}

	dbExecutor := &mockPkgSynchronizer{packages: []db.IPackage{}}

	err = UpdateCache(context.Background(), doer, dbExecutor, "https://aur.archlinux.org", completionPath, nil)
	assert.Error(t, err)
}

func Test_createAURListWriteError(t *testing.T) {
	t.Parallel()

	doer := &mockDoer{
		t:                t,
		wantURL:          "https://aur.archlinux.org/packages.gz",
		returnStatusCode: 200,
		returnBody:       []byte("# Comment\npkg1\npkg2\n"),
		returnErr:        nil,
	}

	err := createAURList(context.Background(), doer, "https://aur.archlinux.org", &errorWriter{}, nil)
	assert.EqualError(t, err, "write error")
}
